/*
 * This file is part of JFlowMap.
 *
 * Copyright 2009 Ilya Boyandin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jflowmap.views.flowstrates;

import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.WindowListener;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

import jflowmap.ColorSchemes;
import jflowmap.FlowEndpoint;
import jflowmap.FlowMapAttrSpec;
import jflowmap.FlowMapColorSchemes;
import jflowmap.FlowMapGraph;
import jflowmap.FlowMapGraphAggLayers;
import jflowmap.data.EdgeListFlowMapStats;
import jflowmap.data.FlowMapNodeTotals;
import jflowmap.data.FlowMapStats;
import jflowmap.data.ValueStatistics;
import jflowmap.data.ViewConfig;
import jflowmap.model.geo.MapProjection;
import jflowmap.model.graph.Graph;
import jflowmap.model.map.GeoMap;
import jflowmap.model.ontology.Dimension;
import jflowmap.util.ColorUtils;
import jflowmap.util.Comment;
import jflowmap.util.HelpTextSwitcher;
import jflowmap.util.piccolo.PBoxLayoutNode;
import jflowmap.util.piccolo.PButton;
import jflowmap.util.piccolo.PNodes;
import jflowmap.views.AbstractCanvasView;
import jflowmap.views.ColorCodes;
import jflowmap.views.VisualCanvas;
import jflowmap.views.flowmap.ColorSchemeAware;

import org.apache.log4j.Logger;

import prefuse.data.Edge;
import prefuse.util.ColorLib;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PCanvas;
import edu.umd.cs.piccolo.PLayer;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.event.PInputEventFilter;
import edu.umd.cs.piccolo.event.PPanEventHandler;
import edu.umd.cs.piccolo.nodes.PText;
import edu.umd.cs.piccolo.util.PBounds;
import edu.umd.cs.piccolo.util.PPaintContext;

/**
 * @author Ilya Boyandin
 */
@Deprecated
public class FlowstratesView extends AbstractCanvasView {

	private static final String ACTION_FIT_IN_VIEW = "fitInView";
	private static final String ACTION_FIT_WHOLE_IN_VIEW = "fitWholeInView";
	private static final String ACTION_CLEAR_SELECTION = "clearSelection";

	public static Logger logger = Logger.getLogger(FlowstratesView.class);

	public static final String VIEW_CONFIG_PROP_MAX_VISIBLE_TUPLES = "view.flowstrates.maxVisibleTuples";
	public static final String VIEW_CONFIG_PROP_MSG_ORIGINS_CAPTION = "view.flowstrates.messages.originsMapCaption";
	public static final String VIEW_CONFIG_PROP_MSG_DESTS_CAPTION = "view.flowstrates.messages.destsMapCaption";

	private static final int LEGEND_MARGIN_BOTTOM = 10;
	private static final String CAPTION_NODE_ATTR = "captionNode";

	public final static long fitInViewDuration(boolean animate) { return animate ? 500 : 0; }

	enum Properties {
		CUSTOM_EDGE_FILTER, NODE_SELECTION
	}

	private static final Font CAPTION_FONT = new Font("Arial", Font.BOLD, 23);

	//  private static final boolean SHOW_TIME_CAPTION = true;

	private boolean interpolateColors = true;

	private final FlowstratesStyle style = new DefaultFlowstratesStyle();
	private final FlowMapGraph flowMapGraph;
	// private final PScrollPane scrollPane;

	private JPanel controlPanel;
	private ValueType valueType = ValueType.VALUE;
	private int maxVisibleTuples;

	private ColorSchemes sequentialColorScheme = ColorSchemes.OrRd;
	private ColorSchemes divergingColorScheme = ColorSchemes.RdBu5;

	private final PropertyChangeSupport changes = new PropertyChangeSupport(this);

	private FlowLinesLayerNode flowLinesLayerNode;

	private List<Edge> visibleEdges;
	private Map<Edge, Integer> visibleEdgeToIndex;
	private Predicate<Edge> customEdgeFilter;

	private TemporalViewLayer temporalLayer;

	private final GeoMap areaMap;
	private MapLayer originMapLayer;
	private MapLayer destMapLayer;

	private boolean focusOnVisibleRows = true;

	private double temporalLayerWidthPercentage = 0.4;

	private final ColorSchemeAware mapColorScheme = new ColorSchemeAware() {
		@Override
		public Color getColor(ColorCodes code) {
			return FlowMapColorSchemes.LIGHT_BLUE__COLOR_BREWER.get(code);
		}
	};
	private final FlowMapGraphAggLayers layers;
	private FlowstratesLegend legend;

	private final MapProjection mapProjection;

	private ValueStatistics valueStat;

	private PBoxLayoutNode mainButtonPanel;
	private PBoxLayoutNode originsMapButtonPanel;
	private PBoxLayoutNode destMapButtonPanel;

	private PBoxLayoutNode temporalViewButtonPanel;

	protected ViewLayer mouseOverLayer;
	protected List<ViewLayer> viewLayers;
	private final ViewConfig viewConfig;



	public FlowstratesView(String name, FlowMapGraph fmg, GeoMap areaMap, MapProjection proj, ViewConfig config) {
		super(name);
		
		String aggName = config.getString(ViewConfig.PROP_DATA_AGGREGATOR);
		AggLayersBuilder aggregator;
		if (aggName != null) {
			aggregator = FlowMapGraphAggLayers.createBuilder(aggName);
		} else {
			aggregator = null;
		}

		logger.info("Opening flowstrates view");

		this.flowMapGraph = fmg;
		this.maxVisibleTuples = config.getIntOrElse(FlowstratesView.VIEW_CONFIG_PROP_MAX_VISIBLE_TUPLES, -1);
		this.mapProjection = proj;
		this.areaMap = areaMap;
		this.viewConfig = config;

		if (aggregator == null) {
			aggregator = new DefaultAggLayersBuilder();
		}
		this.layers = aggregator.build(fmg);

		for (FlowMapGraph g : layers.getFlowMapGraphs()) {
			g.addEdgeWeightDifferenceColumns();
			g.addEdgeWeightRelativeDifferenceColumns();

			FlowMapNodeTotals.supplyNodesWithWeightTotals(g);
			FlowMapNodeTotals.supplyNodesWithWeightTotals(g, g.getEdgeWeightDiffAttr());
			FlowMapNodeTotals.supplyNodesWithWeightTotals(g, g.getEdgeWeightRelativeDiffAttrNames());
		}


		beforeInitialize();
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				initialize();
			}
		});
	}

	private void beforeInitialize() {
		VisualCanvas canvas = getVisualCanvas();
		canvas.setAutoFitOnBoundsChange(false);
		canvas.setBackground(style.getBackgroundColor());
		PPanEventHandler panHandler = new PPanEventHandler();
		panHandler.setEventFilter(new PInputEventFilter() {
			@Override
			public boolean acceptsEvent(PInputEvent event, int type) {
				return !event.isControlDown() && // shouldn't pan when using lasso
						(event.getCamera() != getVisualCanvas().getCamera());
			}
		});
		canvas.setPanEventHandler(panHandler);
		canvas.getZoomHandler().setEventFilter(new PInputEventFilter() {
			@Override
			public boolean acceptsEvent(PInputEvent event, int type) {
				return !event.isControlDown() && // shouldn't pan when using lasso
						(event.getCamera() != getVisualCanvas().getCamera());
			}
		});

		canvas.setInteractingRenderQuality(PPaintContext.HIGH_QUALITY_RENDERING);
		canvas.setAnimatingRenderQuality(PPaintContext.HIGH_QUALITY_RENDERING);


		originMapLayer = new MapLayer(FlowstratesView.this, areaMap, FlowEndpoint.ORIGIN);
		destMapLayer = new MapLayer(FlowstratesView.this, areaMap, FlowEndpoint.DEST);
		temporalLayer = new SimpleHeatmapLayer(this);

		viewLayers = ImmutableList.<ViewLayer>of(originMapLayer, temporalLayer, destMapLayer);


		addCaption(originMapLayer.getCamera(),
				viewConfig.getStringOrElse(VIEW_CONFIG_PROP_MSG_ORIGINS_CAPTION, "Origins"));
		addCaption(temporalLayer.getCamera(), " ");
		addCaption(destMapLayer.getCamera(),
				viewConfig.getStringOrElse(VIEW_CONFIG_PROP_MSG_DESTS_CAPTION, "Destinations"));

		PLayer canvasLayer = canvas.getLayer();
		canvasLayer.addChild(temporalLayer.getCamera()); // should come first so that lasso shows above it
		canvasLayer.addChild(originMapLayer.getCamera());
		canvasLayer.addChild(destMapLayer.getCamera());


		flowLinesLayerNode = new FlowLinesLayerNode(FlowstratesView.this);
		getCamera().addChild(flowLinesLayerNode);


		controlPanel = new FlowstratesControlPanel(FlowstratesView.this);

		FlowstratesView.this.valueStat = stdValueStat();

		legend = new FlowstratesLegend(FlowstratesView.this);
		getCamera().addChild(legend);

	}

	/**
	 * This method must run in EDT
	 */
	private void initialize() {
		resetVisibleEdges();

		getCamera().addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				if (evt.getPropertyName() == PCamera.PROPERTY_BOUNDS) {
					layoutChildren();
					fitInView();
				}
			}
		});

		PropertyChangeListener linesUpdater = new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				hideTooltip();

				originMapLayer.updateCentroids();
				destMapLayer.updateCentroids();

				flowLinesLayerNode.updateFlowLines();
			}
		};
		originMapLayer.getCamera().addPropertyChangeListener(PCamera.PROPERTY_VIEW_TRANSFORM, linesUpdater);
		destMapLayer.getCamera().addPropertyChangeListener(PCamera.PROPERTY_VIEW_TRANSFORM, linesUpdater);
		temporalLayer.getCamera().addPropertyChangeListener(PCamera.PROPERTY_VIEW_TRANSFORM, linesUpdater);

		createButtons();
		initKeystrokes();

	}

	private void initKeystrokes() {
		getCamera().addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseMoved(PInputEvent event) {
				for (ViewLayer layer : viewLayers) {
					if (event.getCamera() == layer.getCamera()) {
						mouseOverLayer = layer;
					}
				}
			}
		});

		VisualCanvas canvas = getVisualCanvas();

		canvas.getInputMap().put(KeyStroke.getKeyStroke("F5"), ACTION_FIT_IN_VIEW);
		canvas.getActionMap().put(ACTION_FIT_IN_VIEW, new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (mouseOverLayer != null) {
					mouseOverLayer.fitInView(true, false);
				}
			}
		});

		canvas.getInputMap().put(KeyStroke.getKeyStroke("F6"), ACTION_FIT_WHOLE_IN_VIEW);
		canvas.getActionMap().put(ACTION_FIT_WHOLE_IN_VIEW, new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (mouseOverLayer != null) {
					mouseOverLayer.fitInView(true, true);
				}
			}
		});

		canvas.getInputMap().put(KeyStroke.getKeyStroke("ESCAPE"), ACTION_CLEAR_SELECTION);
		canvas.getActionMap().put(ACTION_CLEAR_SELECTION, new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if (mouseOverLayer == originMapLayer) {
					originMapLayer.setSelectedNodes(null);
				}
				if (mouseOverLayer == destMapLayer) {
					destMapLayer.setSelectedNodes(null);
				}
			}
		});
	}

	private void createButtons() {

		//  Main buttons panel
		mainButtonPanel = new PBoxLayoutNode(PBoxLayoutNode.Axis.X, 5);
		getVisualCanvas().getLayer().addChild(mainButtonPanel);

		final PButton extendButton = new PButton(" <  > ", false);
		extendButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				if (temporalLayerWidthPercentage > 0.4) {
					temporalLayerWidthPercentage = 0.4;
					extendButton.setText(" <  > ");
					layoutChildren();

				} else {
					temporalLayerWidthPercentage = 0.8;
					extendButton.setText(" >  < ");
					layoutChildren();
				}

			}
		});
		mainButtonPanel.addChild(extendButton);

		final PButton diffButton = new PButton("  DIFFERENZ  ", false);

		diffButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				if (getValueType() == ValueType.VALUE) {
					setValueType(ValueType.DIFF);
					diffButton.setText("REL. DIFFERENZ");
				} else if (getValueType() == ValueType.DIFF) {
					setValueType(ValueType.DIFF_REL);
					diffButton.setText("     WERT     ");
				} else if (getValueType() == ValueType.DIFF_REL) {
					setValueType(ValueType.VALUE);
					diffButton.setText("  DIFFERENZ   ");
				}
			}

		});
		mainButtonPanel.addChild(diffButton);

		mainButtonPanel.addChild(new PText("   "));
		mainButtonPanel.addChild(new PText("   "));

		final PButton sortByMaxButton = new PButton("SORT", false);
		mainButtonPanel.addChild(sortByMaxButton);
		sortByMaxButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				setRowOrdering(getRowOrdering());
			}
		});

		//  Origins map buttons
		originsMapButtonPanel = new PBoxLayoutNode(PBoxLayoutNode.Axis.X, 5);
		getVisualCanvas().getLayer().addChild(originsMapButtonPanel);

		PButton originsClearButton = new PButton("CLEAR");
		originsClearButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				originMapLayer.setSelectedNodes(null);
			}
		});
		originsMapButtonPanel.addChild(originsClearButton);

		PButton originsFitButton = new PButton("FIT");
		originsFitButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				originMapLayer.fitInView(true, false);
			}
		});
		originsMapButtonPanel.addChild(originsFitButton);

		//  Destinations map buttons
		destMapButtonPanel = new PBoxLayoutNode(PBoxLayoutNode.Axis.X, 5);
		getVisualCanvas().getLayer().addChild(destMapButtonPanel);


		PButton destClearButton = new PButton("CLEAR");
		destClearButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				destMapLayer.setSelectedNodes(null);
			}
		});
		destMapButtonPanel.addChild(destClearButton);

		PButton destFitButton = new PButton("FIT");
		destFitButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				destMapLayer.fitInView(true, false);
			}
		});
		destMapButtonPanel.addChild(destFitButton);

		//  Temporal view buttons
		temporalViewButtonPanel = new PBoxLayoutNode(PBoxLayoutNode.Axis.X, 5);
		getVisualCanvas().getLayer().addChild(temporalViewButtonPanel);


		PButton tempFitButton = new PButton("FIT");
		tempFitButton.addInputEventListener(new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				temporalLayer.fitInView(true, false);
			}
		});
		temporalViewButtonPanel.addChild(tempFitButton);


		getVisualCanvas().getHelpText().helpFor(diffButton, new HelpTextSwitcher() {
			@Override
			public String getHelpText () {
				switch (getValueType()) {
				case VALUE:
					return "Aktuell: Anzeige der absoluten Werte";
				case DIFF:
					return "Aktuell: Anzeige der absoluten Differenzen";
				default:
					return "Aktuell: Anzeige der relativen Differenzen im Vergleich zum Vorjahr in Prozent";
				}
			}
		});

		getVisualCanvas().getHelpText().helpFor(sortByMaxButton, new HelpTextSwitcher() {
			@Override
			public String getHelpText() {
				return "Sortieren nach absoluten, summierten Werten";
			}
		});


	}

	private void anchorButtonPanels() {
		PNodes.setPosition(mainButtonPanel, temporalLayer.getCamera().getBounds().x + 5, 4);
		PNodes.anchorNodeToBoundsOf(originsMapButtonPanel, originMapLayer.getCamera(),
				PCanvas.RIGHT_ALIGNMENT, PCanvas.BOTTOM_ALIGNMENT, 5, 5);
		PNodes.anchorNodeToBoundsOf(destMapButtonPanel, destMapLayer.getCamera(),
				PCanvas.RIGHT_ALIGNMENT, PCanvas.BOTTOM_ALIGNMENT, 5, 5);
		PNodes.anchorNodeToBoundsOf(temporalViewButtonPanel, temporalLayer.getCamera(),
				PCanvas.RIGHT_ALIGNMENT, PCanvas.BOTTOM_ALIGNMENT, 5, 5);
	}

	public void resetVisibleEdges() {
		this.visibleEdges = null;
		this.visibleEdgeToIndex = null;
		resetValueStat();
		temporalLayer.renew();
		getFlowLinesLayerNode().renewFlowLines();
		updateLegend();
	}

	void updateColors() {
		temporalLayer.updateColors();
		updateLegend();
		getVisualCanvas().repaint();
	}

	public TemporalViewLayer getTemporalLayer() {
		return temporalLayer;
	}

	public MapLayer getMapLayer(FlowEndpoint ep) {
		switch (ep) {
		case ORIGIN: return originMapLayer;
		case DEST: return destMapLayer;
		default: throw new AssertionError();
		}
	}

	public FlowLinesLayerNode getFlowLinesLayerNode() {
		return flowLinesLayerNode;
	}

	ColorSchemeAware getMapColorScheme() {
		return mapColorScheme;
	}

	MapProjection getMapProjection() {
		return mapProjection;
	}

	private void addCaption(PCamera camera, String caption) {
		PText textNode = new PText(caption);
		textNode.setFont(CAPTION_FONT);
		camera.addAttribute(CAPTION_NODE_ATTR, textNode);
		camera.addChild(textNode);
	}

	void updateLegend() {
		legend.update();
	}

	/*@Override
  public String getName() {
    return viewConfig.getName();
  }*/

	@Override
	public String getSpec() {
		return getName(); // TODO: add more info
	}

	public Iterable<String> getAggLayerNames() {
		return layers.getLayerNames();
	}

	FlowMapGraphAggLayers getAggLayers() {
		return layers;
	}

	public void setSelectedAggLayer(String layerName) {
		layers.setSelectedLayer(layerName);
		resetVisibleEdges();
		temporalLayer.fitInView(false, false);
	}

	public void setMaxVisibleTuples(int maxVisibleTuples) {
		if (this.maxVisibleTuples != maxVisibleTuples) {
			this.maxVisibleTuples = maxVisibleTuples;
			resetVisibleEdges();
			temporalLayer.fitInView(false, false);
		}
	}

	public int getMaxVisibleTuples() {
		return maxVisibleTuples;
	}

	public boolean getFocusOnVisibleRows() {
		return focusOnVisibleRows;
	}

	public void setFocusOnVisibleRows(boolean focusOnVisibleRows) {
		if (this.focusOnVisibleRows != focusOnVisibleRows) {
			this.focusOnVisibleRows = focusOnVisibleRows;
			updateColors();
		}
	}

	public void setCustomEdgeFilter(Predicate<Edge> edgeFilter) {
		if (customEdgeFilter != edgeFilter) {
			Predicate<Edge> oldValue = customEdgeFilter;
			this.customEdgeFilter = edgeFilter;
			if (!clearNodeSelection()) {
				updateVisibleEdges();
			}
			changes.firePropertyChange(Properties.CUSTOM_EDGE_FILTER.name(), oldValue, edgeFilter);
		}
	}

	void updateVisibleEdges() {
		updateVisibleEdges(true);
	}

	void updateVisibleEdges(boolean fitInView) {
		resetVisibleEdges();
		if (fitInView) {
			temporalLayer.fitInView(false, false);
		}
	}

	private Predicate<Edge> getEdgePredicate() {
		return new Predicate<Edge>() {
			@Override
			public boolean apply(Edge edge) {
				return
						(customEdgeFilter == null || customEdgeFilter.apply(edge))  &&

						(originMapLayer.isNodeSelectionEmpty()  ||
								originMapLayer.nodeSelectionContains(edge.getSourceNode()))  &&

								(destMapLayer.isNodeSelectionEmpty()  ||
										destMapLayer.nodeSelectionContains(edge.getTargetNode()));
			}
		};
	}

	private List<Edge> getTopEdges(Iterable<Edge> edges) {
		List<Edge> list = Lists.newArrayList(edges);

		// Sort by magnitude
		Collections.sort(list, RowOrderings.MAX_MAGNITUDE_IN_ROW.getComparator(this));

		// Take first maxVisibleTuples
		if (maxVisibleTuples >= 0) {
			if (list.size() > maxVisibleTuples) {
				list = list.subList(0, maxVisibleTuples);
			}
		}
		return list;
	}

	private Iterable<Edge> removeEdgesWithOnlyNaNs(Iterable<Edge> edges) {
		return Iterables.filter(edges, new Predicate<Edge>() {
			@Override public boolean apply(Edge e) { return flowMapGraph.hasNonZeroWeight(e); }
		});
	}

	List<Edge> getVisibleEdges() {
		if (visibleEdges == null) {
			List<Edge> edges = Lists.newArrayList(
					getTopEdges(Iterables.filter(removeEdgesWithOnlyNaNs(layers.getEdges()),
							getEdgePredicate())));

			Collections.sort(edges, rowOrdering.getComparator(this));

			visibleEdges = edges;
			visibleEdgesStats = null;

			if (flowLinesLayerNode != null) {
				flowLinesLayerNode.updatePalette();
			}
		}

		return visibleEdges;
	}

	public int getVisibleEdgeIndex(Edge edge) {
		Integer idx = getOrInitVisibleEdgesToIndex().get(edge);
		if (idx == null) {
			return -1;
		}
		return idx;
	}

	public Edge getVisibleEdge(int index) {
		return getVisibleEdges().get(index);
	}

	public String getEdgeWeightAttr(int index) {
		return getFlowMapGraph().getEdgeWeightAttrs().get(index);
	}

	public int getEdgeWeightAttrIndex(String attr) {
		return getFlowMapGraph().getEdgeWeightAttrs().indexOf(attr);
	}

	private Map<Edge, Integer> getOrInitVisibleEdgesToIndex() {
		if (visibleEdgeToIndex == null) {
			Map<Edge, Integer> map = Maps.newHashMap();
			int row = 0;
			for (Edge e : getVisibleEdges()) {
				map.put(e, row++);
			}
			visibleEdgeToIndex = map;
		}
		return visibleEdgeToIndex;
	}


	public void setValueType(ValueType valueType) {
		if (this.valueType != valueType) {
			this.valueType = valueType;
			resetValueStat();
		}
	}

	public ValueType getValueType() {
		return valueType;
	}

	public double getValue(Edge edge, String attr) {
		ValueType vtype = getValueType();
		FlowMapAttrSpec attrSpec = getFlowMapGraph().getAttrSpec();

		return edge.getDouble(vtype.getColumnValueAttr(attrSpec, attr));
	}

	public void setDivergingColorScheme(ColorSchemes divergingColorScheme) {
		if (this.divergingColorScheme != divergingColorScheme) {
			this.divergingColorScheme = divergingColorScheme;
			updateColors();
		}
	}

	public ColorSchemes getDivergingColorScheme() {
		return divergingColorScheme;
	}

	public void setSequentialColorScheme(ColorSchemes sequentialColorScheme) {
		if (this.sequentialColorScheme != sequentialColorScheme) {
			this.sequentialColorScheme = sequentialColorScheme;
			updateColors();
		}
	}

	public ColorSchemes getSequentialColorScheme() {
		return sequentialColorScheme;
	}

	public void setInterpolateColors(boolean interpolateColors) {
		if (this.interpolateColors != interpolateColors) {
			this.interpolateColors = interpolateColors;
			updateColors();
		}
	}

	public boolean getInterpolateColors() {
		return interpolateColors;
	}

	public FlowMapGraph getFlowMapGraph() {
		return flowMapGraph;
	}

	public void setShowLinesForHighligtedOnly(boolean showLinesForHighligtedOnly) {
		//    this.showFlowLinesForHighligtedNodesOnly = showLinesForHighligtedOnly;
		flowLinesLayerNode.renewFlowLines();
	}

	/**
	 * @return True if the selection was not empty
	 */
	private boolean clearNodeSelection() {
		if (originMapLayer.clearNodeSelection()  ||  destMapLayer.clearNodeSelection()) {
			updateVisibleEdges();
			return true;
		} else {
			return false;
		}
	}

	public void clearFilters() {
		clearNodeSelection();
		setCustomEdgeFilter(null);
	}

	public boolean isFilterApplied() {
		return
				!originMapLayer.isNodeSelectionEmpty()  ||
				!destMapLayer.isNodeSelectionEmpty() ||
				customEdgeFilter != null;
	}

	//  @Override
	//  public JComponent getViewComponent() {
	//     return scrollPane;
	//  }

	@Override
	public JComponent getControls() {
		//return controlPanel;
		return null;
	}

	public RowOrdering getRowOrdering() {
		return rowOrdering;
	}

	public ValueStatistics getValueStat() {
		return valueStat;
	}

	/**
	 * Used for coloring the heatmap cells, the areas in the maps and the legend.
	 */
	public void setValueStat(ValueStatistics wstat) {
		if (!wstat.equals(valueStat)) {
			this.valueStat = wstat;
			temporalLayer.resetWeightAttrTotals();
			updateColors();
		}
	}

	/**
	 * Reset to the normal value (only those values which are shown in the heatmap).
	 */
	void resetValueStat() {
		setValueStat(stdValueStat());
	}

	private ValueStatistics stdValueStat() {
		return valueType.getSeqStat(getFlowMapStats());
	}

	private FlowMapStats getFlowMapStats() {
		if (focusOnVisibleRows) {
			return getVisibleEdgesStats();
		} else {
			return layers.getStats();
		}
	}

	public FlowMapGraphAggLayers getLayers() {
		return layers;
	}

	private FlowMapStats getVisibleEdgesStats() {
		List<Edge> edges = getVisibleEdges();
		if (visibleEdgesStats == null) {
			visibleEdgesStats = EdgeListFlowMapStats.createFor(edges, flowMapGraph.getAttrSpec());
		}
		return visibleEdgesStats;
	}

	void setEgdeForSimilaritySorting(Edge edge) {
		flowMapGraph.setEgdeForSimilaritySorting(edge);
		updateVisibleEdges();
	}

	public Color getColorFor(double value) {
		return getColorFor(value, getValueStat());
	}

	public Color getColorFor(double value, ValueStatistics wstats) {
		FlowstratesStyle style = getStyle();
		if (Double.isNaN(value)) {
			return style.getMissingValueColor();
		}
		double val = wstats.normalizer().normalizeLogAroundZero(value, true);
		if (wstats.isDiverging()) {
			if (val < -1.0 || val > 1.0) {
				return Color.green;  // out of color scale
			}
			// use diverging color scheme
			return ColorLib.getColor(ColorUtils.colorFromMap(divergingColorScheme.getColors(),
					val, -1.0, 1.0, 255, interpolateColors));
		} else {
			if (val < 0.0 || val > 1.0) {
				return Color.green;
			}
			// use sequential color scheme
			return ColorLib.getColor(ColorUtils.colorFromMap(sequentialColorScheme.getColors(),
					val, 0.0, 1.0, 255, interpolateColors));
		}
	}

	public FlowstratesStyle getStyle() {
		return style;
	}

	@Override
	protected Point2D getTooltipPosition(PNode node) {
		if (PNodes.getRootAncestor(node) == temporalLayer) {
			PBounds bounds = node.getGlobalBounds();
			temporalLayer.getCamera().viewToLocal(bounds);
			temporalLayer.getCamera().localToGlobal(bounds);
			return new Point2D.Double(bounds.getMaxX(), bounds.getMaxY());
		} else {
			return super.getTooltipPosition(node);
		}
	}

	private boolean fitInViewOnce = false;
	private RowOrdering rowOrdering = RowOrderings.MAX_MAGNITUDE_IN_ROW;
	private FlowMapStats visibleEdgesStats;

	@Override
	public void fitInView() {
		if (!fitInViewOnce) {
			fitAllInView();
		}
		flowLinesLayerNode.updateFlowLines();
	}

	private void layoutChildren() {
		final double mapLayerWidthPercent = (1.0 - this.temporalLayerWidthPercentage) / 2;

		layoutCameraNode(originMapLayer.getCamera(), PCanvas.LEFT_ALIGNMENT, PCanvas.TOP_ALIGNMENT,
				mapLayerWidthPercent, 1.0);
		layoutCameraNode(temporalLayer.getCamera(), PCanvas.CENTER_ALIGNMENT, PCanvas.CENTER_ALIGNMENT,
				temporalLayerWidthPercentage, 1.0);
		layoutCameraNode(destMapLayer.getCamera(), PCanvas.RIGHT_ALIGNMENT, PCanvas.TOP_ALIGNMENT, 
				mapLayerWidthPercent, 1.0);

		anchorButtonPanels();

		PBounds lb = legend.getFullBoundsReference();
		PBounds vb = getCamera().getViewBounds();
		PNodes.moveTo(legend, legend.getX(), (vb.getMaxY() - lb.getHeight()) - lb.getY() - LEGEND_MARGIN_BOTTOM);

		flowLinesLayerNode.updateFlowLines();
	}

	private void fitAllInView() {
		originMapLayer.fitInView(false, true);
		temporalLayer.fitInView(false, false);
		destMapLayer.fitInView(false, false);

		fitInViewOnce = true;
	}

	@Override
	protected TooltipText getTooltipTextFor(PNode node) {
		if (node instanceof HeatmapCell) {
			HeatmapCell cell = (HeatmapCell)node;
			return new TooltipText(getFlowMapGraph(), cell.getEdge(), cell.getWeightAttr());
		}
		return super.getTooltipTextFor(node);
	}

	private void layoutCameraNode(PCamera camera, float halign, float valign, double hsizeProportion,
			double vsizeProportion) {

		PBounds globalViewBounds = getCamera().getViewBounds();

		double topMargin4Caption = 0;

		// reserve space for the caption header
		PText caption = (PText) camera.getAttribute(CAPTION_NODE_ATTR);
		if (caption != null) {
			PBounds capb = caption.getFullBoundsReference();
			topMargin4Caption = capb.getMaxY() + capb.getHeight() * .5;
			globalViewBounds.y += topMargin4Caption;
			globalViewBounds.height -= topMargin4Caption;
		}

		// align the camera node
		PBounds viewBounds = camera.getViewBounds();
		PNodes.alignNodeInBounds_bySetBounds(camera, globalViewBounds, halign, valign, hsizeProportion,
				vsizeProportion);
		camera.setViewBounds(viewBounds);

		// align caption
		if (caption != null) {
			PBounds capb = caption.getFullBoundsReference();
			PBounds camb = camera.getBoundsReference();
			PNodes.setPosition(caption, camb.x + (camb.width - capb.width) / 2,
					(topMargin4Caption - capb.height) / 2);
		}
	}

	public void setRowOrdering(RowOrdering rowOrder) {
		if (this.rowOrdering != rowOrder) {
			this.rowOrdering = rowOrder;
			flowMapGraph.setEgdeForSimilaritySorting(null);
			updateVisibleEdges(false);
		}
	}

	public void addPropertyChangeListener(Properties prop, PropertyChangeListener listener) {
		changes.addPropertyChangeListener(prop.name(), listener);
	}

	void fireNodeSelectionChanged(List<String> old, List<String> nodeIds) {
		changes.firePropertyChange(Properties.NODE_SELECTION.name(), old, nodeIds);
	}

	public List<String> getEdgeWeightAttrs() {
		return flowMapGraph.getEdgeWeightAttrs();
	}

	@Override
	public List<Comment> getComments() {
		return new Vector<Comment>();
	}

	@Override
	public PCanvas startExport() {
		return temporalLayer.startExport();
	}

	@Override
	public void endExport() {
		temporalLayer.endExport();
	}

	@Override
	public String getExportName() {
		return temporalLayer.getExportName();
	}

	@Override
	public ViewConfig getViewConfig() {
		return this.viewConfig;
	}

	@Override
	public PCanvas exportLegend() {
		return temporalLayer.exportLegend();
	}

	public FlowstratesLegend getLegend() {
		return legend;
	}

	@Override
	public void constrain(Map<String, Set<Dimension>> dimensions) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public WindowListener getWindowListener() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Graph getCurrentGraph() {
		// TODO Auto-generated method stub
		return null;
	}

}
